package com.nulldev.util.uri.yt;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

import com.nulldev.util.encoding.ObjectUtils;
import com.nulldev.util.java.DataSet;
import com.nulldev.util.java.JVMUtil;
import com.nulldev.util.java.scripting.nScripting;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.math.MathUtil;
import com.nulldev.util.other.ArrayUtils;
import com.nulldev.util.string.StringsUtil;
import com.nulldev.util.web.dns.client.record.A;

public class YTSig {
	
	private static final Logger logger = LoggerFactory.getLoggerD(true, true, true);
	
	/*
	 * var dw={
	 * 		r4:function(a,b){
	 * 			var c=a[0];
	 * 			a[0]=a[b%a.length];
	 * 			a[b%a.length]=c
	 * 		},
	 * 		Mn:function(a,b){
	 * 			a.splice(0,b)
	 * 		},
	 * 		gl:function(a){
	 * 			a.reverse()
	 * 		}
	 * };
	 */
	
	public static class YTSigSoftwareDecoder {
		
		private final String func, code;
		
		public YTSigSoftwareDecoder(final String function, final String code) {
			this.func = function;
			this.code = code;
		}
		
		private String[] actions() {
			final String al = StringsUtil.substringBetweenO(this.code, this.func + "=function(a){", "};");
			return al.split(Pattern.quote(";"));
		}
		
		private String rvFunctionName() {
			final String intfunc = StringsUtil.substringBetweenO(this.code, "={", "};");
			final String[] funcs = intfunc.split(Pattern.quote("},"));
			for (final String func : funcs) {
				if (func.contains("function(a){") && func.contains("a.reverse()")) {
					return func.substring(0, func.indexOf(":"));
				}
			}
			throw new RuntimeException("Unable to find reverse() function!");
		}
		
		private String icFunctionName() {
			final String intfunc = StringsUtil.substringBetweenO(this.code, "={", "};");
			final String[] funcs = intfunc.split(Pattern.quote("},"));
			for (final String func : funcs) {
				if (func.contains("function(a,b){") && func.contains("%")) {
					return func.substring(0, func.indexOf(":"));
				}
			}
			throw new RuntimeException("Unable to find reverse() function!");
		}
		
		private String spFunctionName() {
			final String intfunc = StringsUtil.substringBetweenO(this.code, "={", "};");
			final String[] funcs = intfunc.split(Pattern.quote("},"));
			for (final String func : funcs) {
				if (func.contains("function(a,b){") && func.contains("splice(")) {
					return func.substring(0, func.indexOf(":"));
				}
			}
			throw new RuntimeException("Unable to find reverse() function!");
		}
		
		private static void reverse(String[] str, final int offset) {
			str = (
					StringsUtil.reverse(
							StringsUtil.join(str, "")).split(""));
		}
		
		public static void splice(String[] splice, final int b) {
			splice = YTSig.splice(splice, 0, b);
		}
		
		private static void initChange(String[] a, final int b) {
			final String c = a.clone()[0];
			a[0] = a[b % a.length];
			a[b % a.length] = c;
		}
		
		public String decrypt(final String dec) {
			//FIXME: did reverse() do an oopsie? EDIT: coder is a retard
			JVMUtil.async_println("decipher org: " + dec);
			String[] split = dec.split("");
			final String rv = this.rvFunctionName();
			final String sp = this.spFunctionName();
			final String ic = this.icFunctionName();
			int r = 0;
			final ArrayList<String> acts = ArrayUtils.toArrayList(this.actions());
			acts.remove(0);
			acts.remove(acts.size() - 1); // remove split() and join() from actions list
			for (final String act : acts) {
				final String func = StringsUtil.substringBetweenO(act, ".", "(");
				final int i = MathUtil.safeParseI(StringsUtil.substringBetweenO(act, ",", ")"), -1);
				if (i == -1 || func == null || func.length() == 0) {
					logger.error("[YTSig: Software Decipherer] Skipped a decipher action!");
					continue;
				}
				if (func.equals(rv)) { // was rv
					JVMUtil.async_println("decipher: operation=reverse,i=" + i);
					this.reverse(split, i);
				}else if (func.equals(sp)) {
					JVMUtil.async_println("decipher: operation=split,i=" + i);
					this.splice(split, i);
				}else if (func.equals(ic)) { // was ic
					JVMUtil.async_println("decipher: operation=change,i=" + i);
					this.initChange(split, i);
				}else {
					logger.error("[YTSig: Software Decipherer] Skipped a decipher action! [unknownActionError]");
					continue;
				}
				JVMUtil.async_println("decipher: current val: " + ArrayUtils.join(split, ""));
			}
			JVMUtil.async_println("==============================");
			final String out = ArrayUtils.join(split, "");
			return out;
		}
		
		public void debug() {
			JVMUtil.async_println("YTSig API: Software based decipherer");
			JVMUtil.async_println("Function: " + this.func);
			JVMUtil.async_println("Code: " + this.code);
			JVMUtil.async_println("Decipher functions: ");
			ArrayUtils.printLln(this.actions());
			JVMUtil.async_println("reverseFunction(): " + this.rvFunctionName());
			JVMUtil.async_println("interchangeFunction(): " + this.icFunctionName());
			JVMUtil.async_println("spliceFunction(): " + this.spFunctionName());
		}
	}
	
	public static class YTSignatureDecrypter {
		private final String code, functionName;
		
		private final ScriptEngine ns;
		
		public YTSignatureDecrypter(final String code, final String functionName) {
			this.code = code;
			this.functionName = functionName;
			if (nScripting.cached() == null) {
				throw new RuntimeException("YTSig.error: ScriptEngine API seems to be not useable. (Are you running pre Java 1.7?)");
			}
			this.ns = nScripting.cached().getEngineByName("nashorn");
			if (this.ns != null) {
				try {
					this.ns.eval(this.code);
				} catch (ScriptException e) {
					e.printStackTrace();
				}
			}
		}
		
		public String decrypt(final String signature) {
			if (this.ns == null) {
				return decryptAutoAdaptive(signature, this.code, this.functionName);
			}
			try {
				return (String) this.ns.eval(this.functionName + "(\"" + signature + "\");");
			} catch (ScriptException e) {
				throw new RuntimeException(e);
			}
		}
		
		public String slowerDecrypt(final String signature) {
			return decryptAutoAdaptive(signature, this.code, this.functionName);
		}
		
		public String getCode() {
			return this.code;
		}
		
		public String functionName() {
			return this.functionName;
		}
	}
	
	public static YTSignatureDecrypter accquire(final String jsFile) {
		final DataSet<String, String> _aq = __accquire(jsFile);
		return new YTSignatureDecrypter(_aq.getA(), _aq.getB());
	}
	
	public static YTSigSoftwareDecoder accquireSoft(final String jsFile) {
		final DataSet<String, String> _aq = __accquire(jsFile);
		return new YTSigSoftwareDecoder(_aq.getB(), _aq.getA());
	}
	
	public static DataSet<String, String> __accquire(final String jsFile) {
		final String subStringToValue = StringsUtil.substringBetweenO(jsFile, "=function(a){a=a.split(\"\");",
        		";return a.join(\"\")};");
        if (subStringToValue == null) {
            logger.debug("YouTubeExtractor.sigBack: METHOD was not found.");
            return null;
        }
        logger.debug("YouTubeExtractor.sigBack: Result of new method: " + subStringToValue);
        logger.debug("Back tracking to get function name...");
        final int index = jsFile.indexOf(subStringToValue);
        logger.debug("YouTubeExtractor.sigBack.traceBack: Index: " + index);
        final String r = jsFile.substring(index - "=function(a){a=a.split(\"\");".length() - 2, index - "=function(a){a=a.split(\"\");".length());
        logger.debug("Function name: " + r);
        logger.debug("Generating function...");
        final String function = r + "=function(a){a=a.split(\"\");" + subStringToValue + ";return a.join(\"\")" + "};";
        logger.debug("Function to get signature: " + function);
        logger.debug("Getting required sub class...");
        final String subClassName = subStringToValue.substring(0, 2);
        logger.debug("Required Subclass: " +  subClassName);
        // subclass pattern = var XX={...}};g.IK.prototype[...]
        final String subClass0 = StringsUtil.substringBetween(jsFile, "var " + subClassName + "={", "};g.");
        logger.debug("Generating subclass...");
        final String subClass = "var " + subClassName + "={" + subClass0 + "};";
        logger.debug("Creating decryption script...");
        final String ds = subClass + function;
        if (ds.length() > 1024) {
        	logger.error("Couldn't get decryption function...");
        	return null;
        }
        logger.debug(ds);
        return new DataSet<String, String>(ds, r);
	}
	
	/**
	 * Initally, GK.VC(a, 2); --> a.splice(0, 2); <br>
	 * Translated to ArrayUtils.addAt(a, b);
	 * @param a
	 * @param b
	 * @return
	 */
	@Deprecated
//	public static <T> T[] splice(final T[] a, final int b) {
//		return ArrayUtils.from(a, b);
//	}
	
	public static String[] jd0(String[] s, int i) {
		final ArrayList<String> arr = ArrayUtils.toArrayList(s);
		Collections.reverse(arr);
		return ArrayUtils.arrayListToArray(arr);
	}
	
	public static String[] jd(String[] s, int i) {
		//yc(s);
		//yc(s);
		//return ad(i, null);
		JVMUtil.async_error("jd.debug: s.length=" + s.length);
		JVMUtil.async_error("jd.debug: i=" + i);
		return splice(s, 0, i);
	}
	
	private static void yc(String[] s) {
		//TODO: Implement from base.js
	}

	public static Object[] UY(Object[] a, final int b) {
		final Object c = a[0]; 
		a[0] = a[b % a.length]; 
		a[b % a.length] = c;
		return a;
	} 
	
	@Deprecated
	public static String decrypt(String a) {
//		a = ((String) a).split("");
//		//ObjectUtils.safePrint(a);
//		a = splice((String[]) a, 2);
//		//ObjectUtils.safePrint(a);
//		a = UY((Object[]) a, 8);
//		//ObjectUtils.safePrint(a);
//		a = jd((String[]) a, 10);
//		//ObjectUtils.safePrint(a);
//		a = UY((Object[]) a, 43);
//		//ObjectUtils.safePrint(a);
//		a = splice((String[]) a, 2);
//		//ObjectUtils.safePrint(a);
//		a = UY((Object[]) a, 11);
//		//ObjectUtils.safePrint(a);
//		a = jd((String[]) a, 33);
//		//ObjectUtils.safePrint(a);
//		return ArrayUtils.join((String[]) a, "");
		// TODO: Implement local version of signature generator. EDIT: May not be possible due to constant changing variables
		final ScriptEngineManager sem = nScripting.cached();
		final ScriptEngine se = sem.getEngineByName("nashorn");
		try {
			final String a0 = ((String)a).replaceAll("\n", "~");
			final String ar = ((String) (se.eval("var GK={sU:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c}," + 
					"d2:function(a,b){a.splice(0,b)}," + 
					"az:function(a){a.reverse()}};"
					+ "HK=function(a){a=a.split(\"\");GK.d2(a,3);GK.az(a,10);GK.sU(a,33);GK.sU(a,58);GK.sU(a,7);GK.d2(a,3);GK.sU(a,11);GK.d2(a,1);return a.join(\"\")};"
					+ "HK(\"" + a0 + "\");")));
			return ar.replaceAll("~", "\n");
		} catch (ScriptException e) {
			throw new RuntimeException(e);
		}
	}
	
	public static String decryptAutoAdaptive(String a, final String script, final String funcName) {
		final ScriptEngineManager sem = nScripting.cached();
		final ScriptEngine se = sem.getEngineByName("nashorn");
		try {
			final String a0 = ((String)a).replaceAll("\n", "~");
			final String ar = ((String) se.eval(script + "" + funcName + "(\"" + a0 + "\");"));
			return ar.replaceAll("~", "\n");
		}catch (ScriptException e) {
			throw new RuntimeException(e);
		}
	}
	
	public static <T>T[] splice(final T[] array, int start) {
	    if (start < 0)
	      start += array.length;

	    return splice(array, start, array.length - start);
	  }

	  @SuppressWarnings("unchecked")
	  public static <T> T[] splice(final T[] array, int start, final int deleteCount) {
		if ((array.length - deleteCount) < array.length) {
			return array;
		}  
		  
	    if (start < 0)
	      start += array.length;

	    final T[] spliced = (T[]) Array.newInstance(array.getClass().getComponentType(), array.length - deleteCount);
	    if (start != 0)
	      System.arraycopy(array, 0, spliced, 0, start);

	    if (start + deleteCount != array.length)
	      System.arraycopy(array, start + deleteCount, spliced, start, array.length - start - deleteCount);

	    return spliced;
	  }

	  @SuppressWarnings("unchecked")
	  public static <T>T[] splice(final T[] array, int start, final int deleteCount, final T ... items) {
	    if (start < 0)
	      start += array.length;

	    final T[] spliced = (T[])Array.newInstance(array.getClass().getComponentType(), array.length - deleteCount + items.length);
	    if (start != 0)
	      System.arraycopy(array, 0, spliced, 0, start);

	    if (items.length > 0)
	      System.arraycopy(items, 0, spliced, start, items.length);

	    if (start + deleteCount != array.length)
	      System.arraycopy(array, start + deleteCount, spliced, start + items.length, array.length - start - deleteCount);

	    return spliced;
	  }
}
